Creating headless API to serve content.
# help-with-umbraco
s
Hi! I'm working on creating a headless API to serve content, settings, and translations etc. While the Content Delivery API is excellent, it doesn't fully meet my requirements. How do you usually approach building something like this? One idea I had was to create custom controllers, utilize the Content Delivery API within them, and combine that with data from other services. However, this approach feels a bit off to me. Is there a better way?
k
on our setup we have a single template/controller (JsonPageController), and then we have mappers which uses an interface with 2 methods; CanHandle(PropertyContext context) - checks if the mapper can map the property Map(PropertyContext context) - does the actual mapping It's all mapped into a Dictionary I believe, and then just returned in the controller as Json in the end 🙂
p
We've found the same, the Delivery API does not suit our needs and we want more structured responses. In our case we have implemented the following: -
IApiContentResponseBuilder
(implementation inherits
ApiContentBuilderBase<IApiContentResponse>
) -
IOutputExpansionStrategy
-
IApiPropertyRenderer
Our controllers inherit from
ContentApiControllerBase
. We have our own implementations of
ByRouteContentApiController
and
ByIdContentApiController
which inherit the core ones and extend them where required. We are trying to utilise as much of the existing Delivery API as we can to avoid reinventing the wheel and causing ourselves headaches further down the line.
s
I tend to want finer control over different types without losing Umbraco goodness, so I just use render controllers 🙂
Copy code
cs
public class HomePageController : BasePageController
{
    private readonly IHomePageService _homePageService;

    public HomePageController(...removed for brevity) : base(...removed for brevity)
    {
        _homePageService = homePageService;
    }

    public async Task<ActionResult> HomePage()
    {
        if (CurrentPage is not HomePage page)
        {
            return StatusCode((int)HttpStatusCode.NotFound);
        }

        var responseModel = await TryGetApiResponseModelAsync(()
            => _homePageService.GetApiResponseModelAsync(page));

        return StatusCode((int)responseModel.StatusCode, responseModel);
    }
}
Copy code
cs
public class BasePageController : RenderController
{
    public BasePageController(...removed for brevity): base(...removed for brevity)
    
    {
        _logger = logger;
    }

    public async Task<ApiResponseViewModel> TryGetApiResponseModelAsync(Func<Task<ApiResponseViewModel>> operation)
    {
        var result = new ApiResponseViewModel();

        try
        {
            result = await operation.Invoke();
        }
        catch (Exception? e)
        {
            result = HandleException(result, e);
        }

        return result;
    }
// rest of base methods
m
I have played with the IPublishedCache to try to get the same content as with the template/razor pages
Copy code
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;

namespace Custom.Controllers;

[ApiController]
[Route("/umbraco/api/mikkel")]
public class MikkelController(
    IDocumentNavigationQueryService documentNavigationQueryService,
    IPublishedContentCache publishedContentCache) : Controller
{
    [HttpGet("getall")]
    public ActionResult<string> GetAll()
    {
        if (!documentNavigationQueryService.TryGetRootKeys(out var rootKeys))
        {
            return this.Problem("Error");
        }

        if (documentNavigationQueryService.TryGetDescendantsKeysOfType(rootKeys.First(), "teamPage", out var descendantsKeys))
        {
            if (descendantsKeys.Any())
            {
                var currentNode = publishedContentCache.GetById(descendantsKeys.First());
            }
        }

        var rootNode = publishedContentCache.GetById(rootKeys.First());

        return Ok(rootNode.Name);
    }
}
b
we've done almost exactly the same thing (custom controllers but inheriting from
ContentApiControllerBase
) as we wanted to control some of the redirecting/404 resolvers aspects, and also the returned model structures. With keeping as much as possible from the original solution. Back to the original question a bit, I don't think this is a bad approach though. Quite many things we wanted to do would belong on the controller level, and I don't think making them extensible with all the context they store would be better/more developer friendly in the end than to provide the possibility to roll with your own and then do as you wish there 🙂
9 Views